﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace Konveyer.Core
{
    using Konveyer.Core.Attributes;
    using Konveyer.Core.Interfaces;

    /// <summary>
    /// Runs a given Request  
    /// </summary>
    public class Runner : IRunner
    {
        private IRequest _request;
        private Action<IRequest> _onRequestStart;
        private Action<IRequest> _onRequestEnd;
        private Action<IRequest> _onException;
        private Action<IWidget, IRequest> _onWidgetStarts;
        private Action<IWidget, IRequest> _onWidgetEnds;
        private readonly IInstanceProvider _instanceProvider;

        /// <summary>
        /// Creates a runner with an instance provider
        /// </summary>
        /// <param name="instanceProvider"></param>
        public Runner(IInstanceProvider instanceProvider = null)
        {
            this._instanceProvider = instanceProvider;
        }

        /// <summary>
        /// Adds the request 
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public IRunner With(IRequest request)
        {
            _request = request;
            return this;
        }

        /// <summary>
        /// Adds a listener for when the request is started
        /// </summary>
        /// <param name="onRequestStart"></param>
        /// <returns></returns>
        public IRunner WhenStarts(Action<IRequest> onRequestStart)
        {
            _onRequestStart = onRequestStart;
            return this;
        }

        /// <summary>
        /// Adds a listener for when the request is ended
        /// </summary>
        /// <param name="onRequestEnd"></param>
        /// <returns></returns>
        public IRunner WhenEnds(Action<IRequest> onRequestEnd)
        {
            _onRequestEnd = onRequestEnd;
            return this;
        }

        /// <summary>
        /// Adds a listener for when an exception occurs
        /// </summary>
        /// <param name="onException"></param>
        /// <returns></returns>
        public IRunner WhenExceptionIsThrown(Action<IRequest> onException)
        {
            _onException = onException;
            return this;
        }

        /// <summary>
        /// Adds a listener for when a Widget starts
        /// </summary>
        /// <returns></returns>
        public IRunner WhenWidgetStarts(Action<IWidget, IRequest> onWidgetStarts)
        {
            _onWidgetStarts = onWidgetStarts;
            return this;
        }

        /// <summary>
        /// Adds a listener for when a Widget ends
        /// </summary>
        /// <returns></returns>
        public IRunner WhenWidgetEnds(Action<IWidget, IRequest> onWidgetEnds)
        {
            _onWidgetEnds = onWidgetEnds;
            return this;
        }

        /// <summary>
        /// Runs the runner, if no request is given, runner will expect one to be given via With method
        /// </summary>
        /// <param name="req"></param>
        public virtual void Execute(IRequest req = null)
        {
            if (req != null) _request = req;
            OnRequestStart();            
          

            // Build Belt 
            var beltEntries = GetBeltEntries(this._request);    
            Run(beltEntries, this._request);

            OnRequestEnd();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private IEnumerable<BeltEntries> GetBeltEntries(IRequest request)
        {
            var rtn = new List<BeltEntries>();

            var tmp = request.GetType().GetCustomAttributes(typeof(BeltAttribute), true);
         
            var attributes = tmp.Cast<BeltAttribute>();
            foreach (var a in attributes)
            {
                var entry = new BeltEntries {Attribute = a};

                try
                {
                    entry.Widget = (IWidget)_instanceProvider.Get(entry.Attribute.Widget);
                    rtn.Add(entry);
                }
                catch (Exception ex)
                {
                    throw new InvalidCastException("Unable to create an object of type " + entry.Attribute.Widget.FullName + ". This type may be erroring on the constructor , or may not be of type " + typeof(IWidget).FullName, ex);
                }
            }

            rtn = rtn.OrderBy(x => x.Attribute.Step).ToList();
            return rtn;
        }
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="beltEntries"></param>
        /// <param name="request"></param>
        private void Run(IEnumerable<BeltEntries> beltEntries, IRequest request)
        {
            var steps = (OperationStep[])Enum.GetValues(typeof(OperationStep));
            var sortedSteps = steps.OrderBy(x => x);
            foreach (var s in sortedSteps)
            {
                var currentStep = (OperationStep) s;
                request.CurrentStep = currentStep; 
                foreach(var entry in beltEntries.Where(x => x.Attribute.Step == currentStep))
                {
                    if (entry.Widget != null && request.Exception == null)
                    {
                        try
                        {
                            if (_onWidgetStarts != null) 
                            {
                                _onWidgetStarts(entry.Widget, request);
                            }
                            OnWidgetDoWork(entry.Widget, request);
                            if (_onWidgetEnds != null)
                            {
                                _onWidgetEnds(entry.Widget, request);
                            }
                        }
                        catch (Exception ex)
                        {
                            OnException(request, ex);                                
                        }                        
                    }  
                    else if (request.Exception != null)
                    {
                        OnException(request, request.Exception);
                    }
                }
            }
        }

        protected virtual void OnWidgetDoWork(IWidget widget, IRequest request)
        {
            widget.DoWork(request);
        }

        protected virtual void OnRequestEnd()
        {
            if (_onRequestEnd != null) _onRequestEnd(this._request);
        }

        protected virtual void OnRequestStart()
        {
            if (_request == null) throw new ArgumentException("No request supplied");
            if (_onRequestStart != null) _onRequestStart(this._request);
        }

        protected virtual void OnException(IRequest request, Exception ex)
        {
            if (ex == null || request == null)
                return; 

            request.Exception = ex;
            if (_onException != null)
            {
                _onException(request);
            }
        }
                

        // internal classes 

        class BeltEntries
        {
            public BeltAttribute Attribute { get; set; }
            public IWidget Widget { get; set; }
        }
    }
}
